package net.neocat.tcx.file;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import net.neocat.tcx.util.DomUtils;
import net.neocat.tcx.util.GeoUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;
import java.util.logging.LogManager;
import java.util.logging.Logger;

import static net.neocat.tcx.util.DomUtils.*;

/**
 * Created with IntelliJ IDEA.
 * User: jschek
 * Date: 4/24/13
 * Time: 11:28 PM
 * To change this template use File | Settings | File Templates.
 */
public class TcxReader {

    private static final Logger LOGGER = Logger.getLogger(TcxReader.class.getName());

    private static final ThreadLocal<DateFormat> DATE_FORMAT = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        }
    };
    private static final ThreadLocal<DateFormat> DATE_FORMAT_WITH_MS = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
        }
    };

    private static final ImmutableSet<String> COURSE_NODE_NAMES = ImmutableSet.of("Activity","Course");

    public TcxCourse readFile(File tcxFile) throws IOException, TcxException {
        return readStream(Files.newInputStreamSupplier(tcxFile).getInput());
    }

    public TcxCourse readStream(InputStream inputStream) throws IOException, TcxException {
        try {
            DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
            Document doc = docBuilder.parse(inputStream);
            return readDocument(doc);
        } catch (SAXException | ParserConfigurationException e) {
            throw new IOException("Error parsing XML.", e);
        }
    }

    public TcxCourse readDocument(Document document) throws IOException, TcxException {
        Element trainingCenterDatabaseElement = document.getDocumentElement();
        NodeList coursesElements = trainingCenterDatabaseElement.getElementsByTagName("Courses");
        if(isNullOrEmpty(coursesElements)) {
            coursesElements = trainingCenterDatabaseElement.getElementsByTagName("Activities");
            if(!isNullOrEmpty(coursesElements)) {
                LOGGER.warning("Courses element missing. Using Activities instead.");
            }
        }
        if (isNullOrEmpty(coursesElements)) {
            throw new TcxException("Document is missing the Courses element.");
        }
        if (coursesElements.getLength() > 1) {
            throw new TcxException("More than one Courses element.");
        }
        Element coursesElement = (Element) coursesElements.item(0);
        for (Element courseElement : asElements(iterateChildren(coursesElement))) {
            if (COURSE_NODE_NAMES.contains(courseElement.getNodeName())) {
                return parseCourse(courseElement);
            } else {
                throw new TcxException("Expected to find Course element, but found " + courseElement.getNodeName() + " instead.");
            }
        }
        return null;
    }

    private TcxCourse parseCourse(Element courseElement) throws TcxException {
        final String courseName = getFirstMatchingChild(courseElement, "Name").getTextContent();
        Element lapElement = getFirstMatchingChild(courseElement, "Lap");
        final int totalTimeSeconds = (int) Double.parseDouble(getFirstMatchingChildText(lapElement, "TotalTimeSeconds"));
        final double totalDistanceMeters = Double.parseDouble(getFirstMatchingChildText(lapElement, "DistanceMeters"));
        final GeoPosition beginPosition = parsePosition((Element) getFirstMatchingChild(lapElement, "BeginPosition"), 0);
        final GeoPosition endPosition = parsePosition((Element) getFirstMatchingChild(lapElement, "EndPosition"), 0);


        List<TrackPoint> trackPoints = Lists.newArrayList();
        Element trackElement = DomUtils.getFirstMatchingChild(courseElement, "Track");
        for (Element trackPointElement : asElements(getElementsByName(trackElement, "Trackpoint"))) {
            TrackPoint trackPoint = parseTrackPoint(trackPointElement);
            if(trackPoint!=null) {
                trackPoints.add(trackPoint);
            }
        }

        List<CoursePoint> coursePoints = Lists.newArrayList();
        Map<GeoPosition, Double> elevations = GeoUtils.toElevationMap(trackPoints);
        for (Element coursePointElement : asElements(getElementsByName(courseElement, "CoursePoint"))) {
            coursePoints.add(parseCoursePoint(coursePointElement, elevations));
        }

        Map<GeoPosition, CoursePoint> coursePointsByPosition = GeoUtils.toMap(coursePoints);
        for (TrackPoint trackPoint : trackPoints) {
            trackPoint.setEssential(coursePointsByPosition.containsKey(trackPoint.getPosition()));
        }


        return new TcxCourse(courseName, totalDistanceMeters, totalTimeSeconds, beginPosition, endPosition, trackPoints, coursePoints);
    }

    private CoursePoint parseCoursePoint(Element coursePointElement, Map<GeoPosition, Double> elevations) throws TcxException {
        final String name = getFirstMatchingChildText(coursePointElement, "Name");
        final long timestamp;
        try {
            timestamp = DATE_FORMAT.get().parse(getFirstMatchingChildText(coursePointElement, "Time")).getTime();
        } catch (ParseException e) {
            throw new TcxException("Could not parse date.", e);
        }

        GeoPosition position = parsePosition((Element) getFirstMatchingChild(coursePointElement, "Position"), 0);
        if (elevations.containsKey(position)) {
            position = new GeoPosition(position.getLongitude(), position.getLatitude(), elevations.get(position));
        }
        final PointType pointType = PointType.valueOf(getFirstMatchingChildText(coursePointElement, "PointType"));
        final String notes = getFirstMatchingChildText(coursePointElement, "Notes");
        return new CoursePoint(name, timestamp, null, position, pointType, notes);
    }

    private TrackPoint parseTrackPoint(Element trackPointElement) throws TcxException {
        final long timestamp;
        try {
            String dateString = getFirstMatchingChildText(trackPointElement, "Time");
            if(dateString.length() > 20) {
                timestamp = DATE_FORMAT_WITH_MS.get().parse(dateString).getTime();
            } else {
                timestamp = DATE_FORMAT.get().parse(dateString).getTime();
            }
        } catch (ParseException e) {
            throw new TcxException("Could not parse date.", e);
        }
        final double altitudeMeters = Double.parseDouble(getFirstMatchingChildText(trackPointElement, "AltitudeMeters"));
        final GeoPosition position = parsePosition((Element) getFirstMatchingChild(trackPointElement, "Position"), altitudeMeters);
        final double distanceMeters = Double.parseDouble(getFirstMatchingChildText(trackPointElement, "DistanceMeters"));
        if(position==null) {
            return null;
        } else {
            return new TrackPoint(timestamp, position, altitudeMeters, distanceMeters);
        }
    }

    private GeoPosition parsePosition(Element positionElement, double elevationMeters) throws TcxException {
        if(positionElement==null) {
            return null;
        }
        try {
            final double longitude = Double.parseDouble(getFirstMatchingChildText(positionElement, "LongitudeDegrees"));
            final double latitude = Double.parseDouble(getFirstMatchingChildText(positionElement, "LatitudeDegrees"));
            return new GeoPosition(longitude, latitude, elevationMeters);
        } catch (RuntimeException e) {
            throw new TcxException(positionElement.toString(), e);
        }
    }
}
